<?
/**
* Project:     SQL Assembler: the PHP based database query assembler/joiner
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*
* For questions, help, comments, discussion, etc., please write to the 
* author...
*
* @link http://www.hardwired.hu/
* @link http://sqlassembler.googlecode.com/
* @copyright 2007 Andras Izsof 
* @author Andras Izsof <flatline at hardwired hu>
* @package SQLAssembler
* @version 0.1
*/

/*

TODO: Sz?tmetszeni Select, Update ?s Insert/Replace-re 

*/
class HW_Db_SQLAssembler_Exception extends Exception{};
class HW_Db_SQLAssembler
{
	private $clausesAllowed = array('C','S','D','I','U','VAL','F','W','G','H','O','L','RAW');
	private $clauseData = array();
	private $variableData;
	private $sanitizeClauseVars = false;

	/**
	* Clause abbreviations
	* 
	*/
	private $clauseList = array
	(
	'C' => '',
	'S' => 'SELECT',
	'D' => 'DELETE FROM',
	'I' => 'INSERT INTO',
	'U' => 'UPDATE',
	'VAL' => '',
	'F' => 'FROM',
	'W' => 'WHERE',
	'G' => 'GROUP BY',
	'H' => 'HAVING',
	'O' => 'ORDER BY',
	'L' => 'LIMIT',
	'RAW' => ''
	);

	/**
	* Default clause separators
	* 
	*/
	private $clauseSeparator = array
	(
	'C' => ' ',
	'S' => ',',
	'D' => '',
	'I' => '',
	'U' => '',
	'VAL' => ',',
	'F' => ',',
	'W' => ' AND ',
	'G' => ',',
	'H' => ' AND ',
	'O' => ',',
	'L' => '',
	'RAW' => ''
	);

	function __call($name, $args)
	{
		if (in_array($name,$this->clausesAllowed))
		{
			//-*-* VARIABLE ARGUMENTS
			//$argsFinal = $criteria, $area, $str, $sep

			//Criteria left out! so its true
			if ( !is_int($args[0]) && !is_bool($args[0]))
			{
				$args = array_merge(array(1),$args);
			} //if

			//Insert the area parameter at index 1, since we magicmap to ::add's behaviour
			array_splice($args,1,0,array($name));

			/*
				VAL needs special treatment
				VAL('col','value')
			*/
			if ($name == 'VAL')
			{
				//If value not set, then check global namespace and if exists, insert
				if (!isset($args[3]) )
				{
					//$this->variableData['global'][$args[2]] - this can't be sanitized :O
					$args[3] = isset($this->variableData['global'][$args[2]]) ? "^{$args[2]}^" : '';
				} //if

				$args[2] = array($args[2],$args[3], (isset($args[4]) ? $args[4] : 1) );
				//shift off last two argument, since its inside the args[2] array
				//array_pop($args);

				//Replace 3rd element with quotes argument - 1: quotes will be added
				//$args[3] = (isset($args[4]) ? $args[4] : 1);

				//shift off last argument
				//array_pop($args);
			} //if

			call_user_func_array(array($this,'add'),$args);
		}
		else
		{
			throw new HW_Db_SQLAssembler_Exception("$name clause not exists!", E_USER_ERROR);
		} //if

		return $this;
	} //function

	public function reset()
	{
		$this->clauseData = array();
		$this->variableData = array();
		
		return $this;
	} //function

	public function setSanitization($b)
	{
		$this->sanitizeClauseVars = ($b ? true : false);
		
		return $this;
	} //function

	public function addVariableData($data, $ns = 'global')
	{
		//XXX: lastmod 20110724
		//$this->variableData[ $ns ] = new ArrayObject((array)$data);
		$this->variableData[ $ns ] = (array)$data;

		return $this;
	} //function

	/**
	* Process all the variables passed via addVariableData and generate values for each of them
	* 
	*/
	public function autoVariablePush($ns = 'global', $opt = array() )
	{
		if (!$ns) $ns = 'global';

		$variableData = $this->variableData[$ns];

		if ( isset($opt['exclude']) && isset($opt['only']) )
			throw new HW_Db_SQLAssembler_Exception('Exclude and only cannot be set together!');

		//Exclude variables
		if (isset($opt['exclude']))
		array_map(
			function($value) use(&$variableData) 
			{
				if (isset($variableData[$value]) )
					unset($variableData[$value]);
			}, $opt['exclude']  );

		//Only... use these variables
		if (isset($opt['only']))
		array_walk($variableData, 
			function(&$v, $k) use (&$opt, &$variableData)
			{ 
				if (!in_array($k,$opt['only']))
					unset($variableData[$k]);
			});

		foreach($variableData as $k => $v)
		{
			$this->__call('VAL',array($k,"^$k^"));
		} //foreach
		
		return $this;
	} //function

	/**
	* Private, coz use the S F W G H O L etc... magic functions
	* 
	* @param boolean Criteria... if true, the sql will be added to the clause
	* @param char $area Clause abbreviation (S, F etc.)
	* @param mixed $str SQL string OR array for VAL or L
	* @param mixed $sep Clause separator... if you assign a join like this one, then the separator must be an empty space not a comma: $this->F(1,' LEFT JOIN macska m USING(macska_id)',' ');
	*/
	private function add($criteria, $area, $str, $sep = '#')
	{
		if ($criteria)
		{
			if ($sep == '#')
				$sep = $this->clauseSeparator[$area];

			//Postgres' OFFSET should be set like: $str = '25 OFFSET 0'
			//Not very neat :P
			switch($area)
			{
				case 'L':
					$this->clauseData[$area] = array();	
				break;

				case 'VAL':
				break;
			} //switch

			//$ref = $this->clauseData[$area];
			$this->clauseData[$area][] = array('q' => $str, 'sep' => ($sep == '#' ? $this->clauseSeparator[$area] : $sep ) );

			return count($this->clauseData[$area])-1;
		} //if
	} //function

	private function genValues($area)
	{
		$cols = $values = '';
		for($i=0; $i < count($this->clauseData['VAL']); $i++)
		{
			$x = &$this->clauseData['VAL'][$i]['q'];

			$cols .= ($cols ? ',' : '') . $x[0];
			$values .= ($values ? ',' : '') . ($x[2] == 0 ? /*NOQUOTES*/ "{$x[1]}" : /*QUOTED*/ "'{$x[1]}'" );

			$colvalue .= ($colvalue ? ',' : '') . ($x[2] == 0 ? /*NOQUOTES*/ "{$x[0]}={$x[1]}" : /*QUOTED*/ "{$x[0]}='{$x[1]}'" );
		} //for
		unset($x);

		$this->clauseData['VAL'] = array();
		switch($area)
		{
			case 'I':
				$this->clauseData['VAL'][0] = array('q' => "($cols) VALUES ($values)", 'sep' => ' ');
			break;

			case 'U':
				$this->clauseData['VAL'][0] = array('q' => "SET $colvalue", 'sep' => ' ');
			break;
		} //switch
	} //if

	function gen()
	{
		$tmp = '';

		/*
		INSERT INTO pina 
		(cols) vALUES (values)

		UPDATE pina 
		SET col1=val1, 
		*/

		if ( isset($this->clauseData['I']) )
			$this->genValues('I');
		elseif( isset($this->clauseData['U']) )
			$this->genValues('U');

/*

array(3) {
  ["C"]=>
  array(1) {
    [0]=>
    array(2) {
      ["q"]=>
      string(30) "forumController::defaultAction"
      ["sep"]=>
      string(1) " "
    }
  }
  ["I"]=>
  array(1) {
    [0]=>
    array(2) {
      ["q"]=>
      string(6) "lofasz"
      ["sep"]=>
      string(0) ""
    }
  }
  ["VAL"]=>
  array(1) {
    [0]=>
    array(2) {
      ["q"]=>
      string(53) "(caul,caul2,caul3) VALUES ('value','value2','value3')"
      ["sep"]=>
      string(1) " "
    }
  }
}

*/

		foreach ( $this->clauseList as $area => $clause )
		{
			if ( isset($this->clauseData[$area]) )
			{
				$clausestr = '';

				$cnt = count($this->clauseData[$area]);
				for ($i=0; $i < $cnt; $i++) 
				{
					$clausestr .= 
					$this->clauseData[$area][$i]['q'] . 
					($i+1 < $cnt ? $this->clauseData[$area][$i]['sep'] : '');
				} //for

				if ($clausestr)
				{
					switch($area)
					{
						case 'C':
							$tmp .= "/* {$clausestr} */";
						break;
						
						default:
							$tmp .= ' ' . $clause . ' ' . $clausestr;
						break;
					} //switch
				}
			} //if
		} //foreach

		//Process clauseData for variables and sanitize them
		$this->processClauseVars($tmp);

		return $tmp;
	} //function

	private function processClauseVars(&$str)
	{
		if ($this->sanitizeClauseVars)
			$strReplace = 'HW_Db_Engine_Abstract::replaceQuote2Double(HW_Db_Engine_Abstract::replaceBackslash($this->clauseVarMapper(\'\1\')))';
		else
			$strReplace = '$this->clauseVarMapper(\'\1\')';

		$str = preg_replace
		(
		'~
		(?<!\\\\)\^			#left boundary: (^) cannot be preceded by a backslash
		(
		(?:[^\^]|\\\\ \^)+	#get all chars except a boundary (^) OR  get backslashed boundaries (\^)
		)
		(?<!\\\\)\^			#right boundary: (^) cannot be preceded by a backslash
		~isxe',$strReplace,$str);

		/*
		NOTE: IF YOU ARE USING WHERE AND OTHER sql clauses without quotes and parentheses, you are a BLOODY IDIOT
		NOTE: NEVER FUCKIN EVER include a variable into a statement like this: 
		
		SELECT * FROM macska WHERE macska_id = ^bestupid^
		
		ALWAYS USE QUOTES - AND PARENTHESES -
		
		SELECT * FROM macska WHERE (macska_id = '^bestupid^')
		
		If you fail to do so, you just won an emblem of stupidity!
		*/
		//preg_replace('~~ise',);
	} //function

	public function clauseVarMapper($varstr)
	{
		//$varstr = cid:separator
		/*
		separator : if an array var found, separator specifies the implode glue
		*/
		$vardata = explode(':',$varstr);
		$varstr = $vardata[0];
		
		if (count($vardata) > 1)
		{
			for($i=1; $i < count($vardata); $i++)
			{
			    $x = &$vardata[$i];

			    //separator=','
			    $xtmp = explode('=',$x);
			    $params[ $xtmp[0] ] = $xtmp[1] ? $xtmp[1] : 1;
			} //for
			unset($x);
		} //if

		$tmp = explode('@',$varstr);

		//It's a moissanite, a FUCK-ALL
		if (!$tmp[0]) return '';

		//No namespace, so set it to global
		if (!$tmp[1])
		{
			$ns = 'global';
			$varstr = $tmp[0];
		} //if
		else
		{
			$ns = $tmp[0];
			$varstr = $tmp[1];
		} //if

		//An object
		if ( isset($this->variableData[$ns][$varstr]) )
		{
			//TODO: array members are NOT CHECKED!
			//Implode them together if its an array
			if (is_array($this->variableData[$ns][$varstr]))
				return implode(($params['separator'] ? $params['separator'] : ','),$this->variableData[$ns][$varstr]);
			else
				return (string)$this->variableData[$ns][$varstr];
		}

		return '';
	} //function
	

	/**
	 * Generate specific section
	 *
	 * @param unknown_type $area
	 */
	//TODO: rewrite
	public function genClause($area, $showsection = 0)
	{
		$tmp = '';
		if ( count($this->clauseData[$area]) )
		{
			for ($i=0; $i < count($this->clauseData[$area]); $i++) 
			{
				$tmp .= $this->clauseData[$area][$i];
			} //for
		} //if
		
		return $tmp;
	} //function

	//TODO: rewrite
	public function clearClause()
	{
		foreach ($this->clauseData as $area => $v)
		{
			//We only care about uppercase variables... do not declare any else variable with ALL UPPERCASE since it will be purged
			if (ctype_upper($area))
			{
				if ($area == 'L')
					$this->clauseData[$area] = '';
				else
					$this->clauseData[$area] = array();
			} //if
		} //foreach
		
		return $this;
	} //function

	//TODO: rewrite
	public function delClauseAtIndex($area,$index)
	{
		if ( isset($this->clauseData[$area][$index]) )
			unset($this->clauseData[$area][$index]);
		else
			trigger_error("Index nr. {$index} not found in {$area}!",E_USER_ERROR);
	} //function

} //_sql

